package jinyilw.tools.net.dev;

import jinyilw.tools.net.file.FileTransfer;

import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

public class TCPClient extends BaseClient
{
	private static final int BUFFER_SIZE = 10240;

	private final SocketChannel socketChannel;
	private final Selector packetReader;

	private ByteBuffer readBuffer;
	private ByteBuffer writeBuffer;

	private DataInputStream input;
	private PipedOutputStream storage;
	private boolean waitingForLength = true;
	private int length;

	private InetAddress clientAddress;
	private InetAddress remoteAddress;

	private boolean enableFileTransfer;
	private FileTransfer fileTransfer;

	public TCPClient(String host, int port) throws IOException
	{
		this(new InetSocketAddress(host, port));
	}

	public TCPClient(SocketAddress host) throws IOException
	{
		socketChannel = SocketChannel.open();
		packetReader = Selector.open();
		try
		{
			socketChannel.connect(host);
		} catch (IOException e)
		{
			close();
			throw e;
		}
		init();
	}

	protected TCPClient(TCPServer server, SocketChannel client,
			Selector packetReader) throws IOException
	{
		super(server);
		this.socketChannel = client;
		this.packetReader = packetReader;
		init();
	}

	private void init() throws IOException
	{
		storage = new PipedOutputStream();
		input = new DataInputStream(
				new PipedInputStream(storage, BUFFER_SIZE * 5));
		readBuffer = ByteBuffer.allocate(BUFFER_SIZE);
		writeBuffer = ByteBuffer.allocate(BUFFER_SIZE);
		readBuffer.order(ByteOrder.BIG_ENDIAN);
		writeBuffer.order(ByteOrder.BIG_ENDIAN);
		socketChannel.configureBlocking(false);
		socketChannel.socket().setTcpNoDelay(true);
		socketChannel.register(packetReader, SelectionKey.OP_READ, this);
		remoteAddress = socketChannel.socket().getInetAddress();
	}

	private void close()
	{
		try
		{
			socketChannel.close();
			packetReader.close();
		} catch (IOException e)
		{
			e.printStackTrace();
		}
	}

	public void setFileTransfer(FileTransfer fileTransfer)
	{
		if (enableFileTransfer)
			this.fileTransfer.stopConnect();
		this.fileTransfer = fileTransfer;
		enableFileTransfer = fileTransfer != null;
		if (enableFileTransfer && server == null)
		{
			fileTransfer.startConnect();
			fileTransfer.setClientID(getClientID());
		}
	}

	@Override
	public void update(long elapsedTime) throws IOException
	{
		super.update(elapsedTime);
		if (packetReader.select() > 0)
		{
			Iterator<SelectionKey> packetIterator = packetReader.selectedKeys().iterator();
			while (packetIterator.hasNext())
			{
				SelectionKey key = packetIterator.next();
				packetIterator.remove();
				read();
			}
		}
	}

	protected synchronized void read() throws IOException
	{
		readBuffer.clear();
		int bytesRead = socketChannel.read(readBuffer);
		if (bytesRead < 0)
		{
			throw new IOException("Reached end of stream");
		} else if (bytesRead == 0)
		{
			return;
		}
		storage.write(readBuffer.array(), 0, bytesRead);

		while (input.available() > 0)
		{
			if (waitingForLength)
			{
				if (input.available() > 2)
				{
					length = input.readShort();
					waitingForLength = false;
				} else
				{
					break;
				}
			} else
			{
				if (input.available() >= length)
				{
					byte[] data = new byte[length];
					input.readFully(data);
					addReceivedPacket(data);
					waitingForLength = true;
				} else
				{
					break;
				}
			}
		}
	}

	@Override
	protected synchronized void sendPacket(byte[] data) throws IOException
	{
		writeBuffer.clear();
		writeBuffer.putShort((short) data.length);
		writeBuffer.put(data);
		writeBuffer.rewind();
		writeBuffer.limit(data.length + 2);
		socketChannel.write(writeBuffer);
		writeBuffer.limit(2048);
	}

	@Override
	protected void disconnectImpl() throws IOException
	{
		socketChannel.close();
		if (enableFileTransfer && server == null)
			fileTransfer.stopConnect();
	}

	@Override
	public boolean isConnected()
	{
		return socketChannel.isConnected();
	}

	@Override
	public String getDetail()
	{
		if (clientAddress == null)
			clientAddress = socketChannel.socket().getLocalAddress();
		if (clientAddress != null)
			return clientAddress.toString();
		return "";
	}

	@Override
	public String getRemoteDetail()
	{
		if (remoteAddress != null)
			return remoteAddress.toString();
		return "";
	}

	@Override
	public void setClientID(short clientID)
	{
		super.setClientID(clientID);
		if (enableFileTransfer && fileTransfer.isConnected())
			fileTransfer.setClientID(clientID);
	}

	@Override
	public File[] getReceivedFiles()
	{
		if (enableFileTransfer)
			return fileTransfer.getReceivedFiles();
		return null;
	}

	@Override
	public void getFiles(String... filePaths)
	{
		if (enableFileTransfer)
			fileTransfer.getFiles(filePaths);
	}

	@Override
	public File[] getFilesAndWait(int timeout, String... fileNames)
	{
		if (enableFileTransfer)
			return fileTransfer.getFilesAndWait(timeout, fileNames);
		return null;
	}

	@Override
	public void sendFiles(File... files)
	{
		if (enableFileTransfer)
			try
			{
				fileTransfer.sendFiles(files);
			} catch (IOException e)
			{
				e.printStackTrace();
			}
	}
}
